/*
 * Fri Feb 28 07:47:05 1997  Doug Lea  (dl at gee)
 * Based on PolyLineFigure
 */

package CH.ifa.draw.contrib;

import java.awt.*;
import java.util.*;
import java.io.IOException;
import CH.ifa.draw.framework.*;
import CH.ifa.draw.util.*;
import CH.ifa.draw.standard.*;
import CH.ifa.draw.figures.*;

/**
 * A scalable, rotatable polygon with an arbitrary number of points
 */
public  class PolygonFigure extends AttributeFigure {

  /**
   * Distance threshold for smoothing away or locating points
   **/
  static final int TOO_CLOSE = 2;

  /*
   * Serialization support.
   */
  private static final long serialVersionUID = 6254089689239215026L;
  private int polygonFigureSerializedDataVersion = 1;

  protected Polygon fPoly = new Polygon();

  public PolygonFigure() {
    super();
  }

  public PolygonFigure(int x, int y) {
    fPoly.addPoint(x, y);
  }

  public PolygonFigure(Polygon p) {
    fPoly = new Polygon(p.xpoints, p.ypoints, p.npoints);
  }

  public Rectangle displayBox() {
    return bounds(fPoly);
  }


  public boolean isEmpty() {
    return (fPoly.npoints < 3 ||
            (size().width < TOO_CLOSE) && (size().height < TOO_CLOSE));
  }

  public Vector handles() {
    Vector handles = new Vector(fPoly.npoints);
    for (int i = 0; i < fPoly.npoints; i++)
      handles.addElement(new PolygonHandle(this, locator(i), i));
    handles.addElement(new PolygonScaleHandle(this));
    //handles.addElement(new PolygonPointAddHandle(this));
    return handles;
  }


  public void basicDisplayBox(Point origin, Point corner) {
    Rectangle r = displayBox();
    int dx = origin.x - r.x;
    int dy = origin.y - r.y;
    fPoly.translate(dx, dy);
    r = displayBox();
    Point oldCorner = new Point(r.x + r.width, r.y + r.height);
    Polygon p = getPolygon();
    scaleRotate(oldCorner, p, corner);
  }

  /**
   * return a copy of the raw polygon
   **/
  public Polygon getPolygon() {
    return new Polygon(fPoly.xpoints, fPoly.ypoints, fPoly.npoints);
  }

  public Point center() {
    return center(fPoly);
  }

  public Enumeration points() {
    Vector pts = new Vector(fPoly.npoints);
    for (int i = 0; i < fPoly.npoints; ++i)
      pts.addElement(new Point(fPoly.xpoints[i], fPoly.ypoints[i]));
    return pts.elements();
  }

  public int pointCount() {
    return fPoly.npoints;
  }

  public void basicMoveBy(int dx, int dy) {
    fPoly.translate(dx, dy);
  }

  public void drawBackground(Graphics g) {
      g.fillPolygon(fPoly);
  }

  public void drawFrame(Graphics g) {
      g.drawPolygon(fPoly);
  }

  public boolean containsPoint(int x, int y) {
    return fPoly.contains(x, y);
  }

  public Connector connectorAt(int x, int y) {
    return new ChopPolygonConnector(this);
  }

  /**
   * Adds a node to the list of points.
   */
  public  void addPoint(int x, int y) {
    fPoly.addPoint(x, y);
    changed();
  }


  /**
   * Changes the position of a node.
   */
  public  void setPointAt(Point p, int i) {
    willChange();
    fPoly.xpoints[i] = p.x;
    fPoly.ypoints[i] = p.y;
    changed();
  }

  /**
   * Insert a node at the given point.
   */
  public  void insertPointAt(Point p, int i) {
    willChange();
    int n = fPoly.npoints + 1;
    int[] xs = new int[n];
    int[] ys = new int[n];
    for (int j = 0; j < i; ++j) {
      xs[j] = fPoly.xpoints[j];
      ys[j] = fPoly.ypoints[j];
    }
    xs[i] = p.x;
    ys[i] = p.y;
    for (int j = i; j < fPoly.npoints; ++j) {
      xs[j+1] = fPoly.xpoints[j];
      ys[j+1] = fPoly.ypoints[j];
    }

    fPoly = new Polygon(xs, ys, n);
    changed();
  }

  public  void removePointAt(int i) {
    willChange();
    int n = fPoly.npoints - 1;
    int[] xs = new int[n];
    int[] ys = new int[n];
    for (int j = 0; j < i; ++j) {
      xs[j] = fPoly.xpoints[j];
      ys[j] = fPoly.ypoints[j];
    }
    for (int j = i; j < n; ++j) {
      xs[j] = fPoly.xpoints[j+1];
      ys[j] = fPoly.ypoints[j+1];
    }
    fPoly = new Polygon(xs, ys, n);
    changed();
  }

  /**
   * Scale and rotate relative to anchor
   **/
  public  void scaleRotate(Point anchor, Polygon originalPolygon, Point p) {
    willChange();

    // use center to determine relative angles and lengths
    Point ctr = center(originalPolygon);
    double anchorLen = Geom.length(ctr.x, ctr.y, anchor.x, anchor.y);

    if (anchorLen > 0.0) {
      double newLen = Geom.length(ctr.x, ctr.y, p.x, p.y);
      double ratio = newLen / anchorLen;

      double anchorAngle = Math.atan2(anchor.y - ctr.y, anchor.x - ctr.x);
      double newAngle = Math.atan2(p.y - ctr.y, p.x - ctr.x);
      double rotation = newAngle - anchorAngle;

      int n = originalPolygon.npoints;
      int[] xs = new int[n];
      int[] ys = new int[n];

      for (int i = 0; i < n; ++i) {
        int x = originalPolygon.xpoints[i];
        int y = originalPolygon.ypoints[i];
        double l = Geom.length(ctr.x, ctr.y, x, y) * ratio;
        double a = Math.atan2(y - ctr.y, x - ctr.x) + rotation;
        xs[i] = (int)(ctr.x + l * Math.cos(a) + 0.5);
        ys[i] = (int)(ctr.y + l * Math.sin(a) + 0.5);
      }
      fPoly =  new Polygon(xs, ys, n);
    }
    changed();
  }


  /**
   * Remove points that are nearly colinear with others
   **/
  public void smoothPoints() {
    willChange();
    boolean removed = false;
    int n = fPoly.npoints;
    do {
      removed = false;
      int i = 0;
      while (i < n && n >= 3) {
        int nxt = (i + 1) % n;
        int prv = (i - 1 + n) % n;

        if ((distanceFromLine(fPoly.xpoints[prv], fPoly.ypoints[prv],
                              fPoly.xpoints[nxt], fPoly.ypoints[nxt],
                              fPoly.xpoints[i], fPoly.ypoints[i]) < TOO_CLOSE)) {
          removed = true;
          --n;
          for (int j = i; j < n; ++j) {
            fPoly.xpoints[j] = fPoly.xpoints[j+1];
            fPoly.ypoints[j] = fPoly.ypoints[j+1];
          }
        }
        else
          ++i;
      }
    } while(removed);
    if (n != fPoly.npoints)
      fPoly =  new Polygon(fPoly.xpoints, fPoly.ypoints, n);
    changed();
  }

  /**
   * Splits the segment at the given point if a segment was hit.
   * @return the index of the segment or -1 if no segment was hit.
   */
  public int splitSegment(int x, int y) {
    int i = findSegment(x, y);
    if (i != -1) {
      insertPointAt(new Point(x, y), i+1);
      return i + 1;
    }
    else
      return -1;
  }

  public Point pointAt(int i) {
    return new Point(fPoly.xpoints[i], fPoly.ypoints[i]);
  }

  /**
   * Return the point on the polygon that is furthest from the center
   **/
  public Point outermostPoint() {
    Point ctr = center();
    int outer = 0;
    long dist = 0;

    for (int i = 0; i < fPoly.npoints; ++i) {
      long d = Geom.length2(ctr.x, ctr.y, fPoly.xpoints[i], fPoly.ypoints[i]);
      if (d > dist) {
        dist = d;
        outer = i;
      }
    }

    return new Point(fPoly.xpoints[outer], fPoly.ypoints[outer]);
  }


  /**
   * Gets the segment that is hit by the given point.
   * @return the index of the segment or -1 if no segment was hit.
   */
  public int findSegment(int x, int y) {
    double dist = TOO_CLOSE;
    int best = -1;

    for (int i = 0; i < fPoly.npoints; i++) {
      int n = (i + 1) % fPoly.npoints;
      double d =  distanceFromLine(fPoly.xpoints[i], fPoly.ypoints[i],
                                   fPoly.xpoints[n], fPoly.ypoints[n],
                                   x, y);
      if (d < dist) {
        dist = d;
        best = i;
      }
    }
    return best;
  }

  public Point chop(Point p) {
    return chop(fPoly, p);
  }

  public void write(StorableOutput dw) {
    super.write(dw);
    dw.writeInt(fPoly.npoints);
    for (int i = 0; i < fPoly.npoints; ++i) {
      dw.writeInt(fPoly.xpoints[i]);
      dw.writeInt(fPoly.ypoints[i]);
    }
  }

  public void read(StorableInput dr) throws IOException {
    super.read(dr);
    int size = dr.readInt();
    int[] xs = new int[size];
    int[] ys = new int[size];
    for (int i = 0; i < size; i++) {
      xs[i] = dr.readInt();
      ys[i] = dr.readInt();
    }
    fPoly = new Polygon(xs, ys, size);
  }

  /**
   * Creates a locator for the point with the given index.
   */
  public static Locator locator(final int pointIndex) {
    return new AbstractLocator() {
      public Point locate(Figure owner) {
        PolygonFigure plf = (PolygonFigure)owner;
        // guard against changing PolygonFigures -> temporary hack
        if (pointIndex < plf.pointCount())
          return ((PolygonFigure)owner).pointAt(pointIndex);
        return new Point(-1, -1);
      }
    };
  }

  /**
   * compute distance of point from line segment, or
   * Double.MAX_VALUE if perpendicular projection is outside segment; or
   * If pts on line are same, return distance from point
   **/
  public static double distanceFromLine(int xa, int ya,
                                        int xb, int yb,
                                        int xc, int yc) {


    // source:http://vision.dai.ed.ac.uk/andrewfg/c-g-a-faq.html#q7
    //Let the point be C (XC,YC) and the line be AB (XA,YA) to (XB,YB).
    //The length of the
    //      line segment AB is L:
    //
    //                    ___________________
    //                   |        2         2
    //              L = \| (XB-XA) + (YB-YA)
    //and
    //
    //                  (YA-YC)(YA-YB)-(XA-XC)(XB-XA)
    //              r = -----------------------------
    //                              L**2
    //
    //                  (YA-YC)(XB-XA)-(XA-XC)(YB-YA)
    //              s = -----------------------------
    //                              L**2
    //
    //      Let I be the point of perpendicular projection of C onto AB, the
    //
    //              XI=XA+r(XB-XA)
    //              YI=YA+r(YB-YA)
    //
    //      Distance from A to I = r*L
    //      Distance from C to I = s*L
    //
    //      If r < 0 I is on backward extension of AB
    //      If r>1 I is on ahead extension of AB
    //      If 0<=r<=1 I is on AB
    //
    //      If s < 0 C is left of AB (you can just check the numerator)
    //      If s>0 C is right of AB
    //      If s=0 C is on AB

    int xdiff = xb - xa;
    int ydiff = yb - ya;
    long l2 = xdiff*xdiff + ydiff*ydiff;

    if (l2 == 0) return Geom.length(xa, ya, xc, yc);

    double rnum =  (ya-yc) * (ya-yb) - (xa-xc) * (xb-xa);
    double r = rnum / l2;

    if (r < 0.0 || r > 1.0) return Double.MAX_VALUE;

    double xi = xa + r * xdiff;
    double yi = ya + r * ydiff;
    double xd = xc - xi;
    double yd = yc - yi;
    return Math.sqrt(xd * xd + yd * yd);

    /*
      for directional version, instead use
      double snum =  (ya-yc) * (xb-xa) - (xa-xc) * (yb-ya);
      double s = snum / l2;

      double l = Math.sqrt((double)l2);
      return = s * l;
      */
  }

  /**
   * replacement for builtin Polygon.getBounds that doesn't always update?
   */

  public static Rectangle bounds(Polygon p) {
	int minx = Integer.MAX_VALUE;
	int miny = Integer.MAX_VALUE;
	int maxx = Integer.MIN_VALUE;
	int maxy = Integer.MIN_VALUE;
	int n = p.npoints;
	for (int i = 0; i < n; i++) {
      int x = p.xpoints[i];
      int y = p.ypoints[i];
      if (x > maxx) maxx = x;
      if (x < minx) minx = x;
      if (y > maxy) maxy = y;
      if (y < miny) miny = y;
    }

	return new Rectangle(minx, miny, maxx-minx, maxy-miny);
  }

  public static Point center(Polygon p) {
    long sx = 0;
    long sy = 0;
    int n = p.npoints;
	for (int i = 0; i < n; i++) {
      sx += p.xpoints[i];
      sy += p.ypoints[i];
    }

	return new Point((int)(sx/n), (int)(sy/n));
  }

  public static Point chop(Polygon poly, Point p) {
    Point ctr = center(poly);
    int cx = -1;
    int cy = -1;
    long len = Long.MAX_VALUE;

    // Try for points along edge

    for (int i = 0; i < poly.npoints; ++i) {
      int nxt = (i + 1) % poly.npoints;
      Point chop = Geom.intersect(poly.xpoints[i],
                             poly.ypoints[i],
                             poly.xpoints[nxt],
                             poly.ypoints[nxt],
                             p.x,
                             p.y,
                             ctr.x,
                             ctr.y);
      if (chop != null) {
        long cl = Geom.length2(chop.x, chop.y, p.x, p.y);
        if (cl < len) {
          len = cl;
          cx = chop.x;
          cy = chop.y;
        }
      }
    }
    // if none found, pick closest vertex
    //if (len ==  Long.MAX_VALUE) {
    { // try anyway
      for (int i = 0; i < poly.npoints; ++i) {
        long l = Geom.length2(poly.xpoints[i], poly.ypoints[i], p.x, p.y);
        if (l < len) {
          len = l;
          cx = poly.xpoints[i];
          cy = poly.ypoints[i];
        }
      }
    }
    return new Point(cx, cy);
  }

}

